package pers.brozen.anka.support.wechat;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import pers.brozen.anka.support.wechat.cache.MetaInfoCache;
import pers.brozen.anka.support.wechat.cache.RequestMetaInfo;
import pers.brozen.anka.support.wechat.request.MiniProgramPayCloseOrderRequest;
import pers.brozen.anka.support.wechat.request.MiniProgramPayOrderQueryRequest;
import pers.brozen.anka.support.wechat.request.MiniProgramPayRefundRequest;
import pers.brozen.anka.support.wechat.request.MiniProgramPayUnifiedOrderRequest;
import pers.brozen.anka.support.wechat.response.MiniProgramPayCloseOrderResponse;
import pers.brozen.anka.support.wechat.response.MiniProgramPayOrderQueryResponse;
import pers.brozen.anka.support.wechat.response.MiniProgramPayRefundResponse;
import pers.brozen.anka.support.wechat.response.MiniProgramPayUnifiedOrderResponse;
import pers.brozen.anka.utils.MD5Utils;
import pers.brozen.anka.utils.TokenGenerate;
import pers.brozen.anka.utils.Tuple;
import pers.brozen.anka.utils.json.JacksonUtils;
import pers.brozen.anka.utils.time.InstantUtils;

import java.io.StringReader;
import java.security.KeyStore;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author devil
 * @date 2020/4/21
 */
@Slf4j
public class WeChatPayAPI extends WeChatAPI {

    /**
     * 订单支付超时时间，从下单到支付多久关闭订单，毫秒
     */
    private static final long PAY_EXPIRY_MILLIS = TimeUnit.DAYS.toMillis(1);

    /**
     * 微信unifiedOrder过期时间，最长是2个小时，毫秒
     */
    private static final long UNIFIED_ORDER_EXPIRY_MILLIS = TimeUnit.HOURS.toMillis(2);

    private TokenGenerate nonceGenerator = new TokenGenerate();

    @Getter
    private String mchId;

    @Getter
    private String mchPayKey;

    /**
     * 证书密码
     */
    private String certPassword;

    private KeyStore keyStore;

    private AES aes;

    public WeChatPayAPI(String appId, String appSecret, String mchId, String mchPayKey, KeyStore keyStore, String certPassword) {
        super(appId, appSecret);
        this.mchId = mchId;
        this.mchPayKey = mchPayKey;
        this.certPassword = certPassword;
        this.keyStore = keyStore;
        this.aes = SecureUtil.aes(SecureUtil.md5(mchPayKey).toLowerCase().getBytes());
    }


    /**
     * 预支付信息
     */
    public Tuple<MiniProgramPayUnifiedOrderRequest, MiniProgramPayUnifiedOrderResponse> unifiedOrder(String outTradeNo, Date createTime, Integer payment, String openId, String callbackUrl) {
        Instant now = Instant.now();
        Instant payExpiryTime = createTime.toInstant().plusMillis(PAY_EXPIRY_MILLIS);
        Instant unifiedOrderExpireTime = now.plusMillis(UNIFIED_ORDER_EXPIRY_MILLIS);

        if (payExpiryTime.toEpochMilli() > now.toEpochMilli()) {
            throw new IllegalStateException("订单已经关闭，请重新下单哦~");
        }

        // unifiedOrder的最长过期时间不能超过订单支付过期时间(下单+24小时)
        if (unifiedOrderExpireTime.toEpochMilli() > payExpiryTime.toEpochMilli()) {
            unifiedOrderExpireTime = payExpiryTime;
        }

        MiniProgramPayUnifiedOrderRequest request = new MiniProgramPayUnifiedOrderRequest();
        request.setAppid(getAppId());
        request.setMchId(mchId);
        request.setNotifyUrl(String.format("%s/wx/pay/notify/%s", callbackUrl, outTradeNo));
        request.setTradeType("JSAPI");
        request.setNonceStr(nonceGenerator.generateSessionId());
        request.setBody("订单支付");
        request.setOutTradeNo(outTradeNo);
        request.setTotalFee(payment.toString());
        request.setTimeStart(InstantUtils.format(createTime.toInstant(), "yyyyMMddHHmmss"));
        // 下单后24小时不支付就订单关闭
        request.setExpireTime(Date.from(unifiedOrderExpireTime));
        request.setTimeExpire(InstantUtils.format(unifiedOrderExpireTime, "yyyyMMddHHmmss"));
        request.setOpenid(openId);
        request.setSpbillCreateIp("39.108.216.0");// TODO 调用接口的服务器ip，先写死
        request.setSignType("MD5");
        request.setSign(sign(request).toUpperCase());

        // 接口返回异常则抛出
        Tuple<MiniProgramPayUnifiedOrderRequest, MiniProgramPayUnifiedOrderResponse> result = new Tuple<>();
        result.setA(request);
        MiniProgramPayUnifiedOrderResponse response = request(request);
        result.setB(response);

        if (!"SUCCESS".equalsIgnoreCase(response.getResultCode())) {
            throw new WeChatException(response.getErrCode(), response.getErrCodeDes());
        }

        return result;
    }

    /**
     * 退款
     * 微信会校验 totalFee refundFee
     * outRefundNo 对应 orderId
     * 同一个 outRefundId 请求能成功，但是退款只有一次
     * 同一个 outRefundId 但是不同 totalFee 或 refundFee 会提示 订单金额或退款金额与之前请求不一致，请核实后再试
     * 如果 transactionId 已经全部退款 订单已全额退款
     * 如果 transactionId 可退金额不足 会提示金额不足
     */
    public Tuple<MiniProgramPayRefundRequest, MiniProgramPayRefundResponse> orderRefund(String outRefundNo, String transactionId, Integer totalFee, Integer refundFee, String reason, String callbackUrl) {
        MiniProgramPayRefundRequest request = new MiniProgramPayRefundRequest();
        request.setAppid(getAppId());
        request.setMchId(mchId);
        request.setNonceStr(nonceGenerator.generateSessionId());

        // 支付号 二选一
        request.setTransactionId(transactionId);
//        request.setOutTradeNo();

        request.setOutRefundNo(outRefundNo);
        request.setTotalFee(totalFee);
        request.setRefundFee(refundFee);
        request.setRefundDesc(reason);
        request.setNotifyUrl(String.format("%s/wx/notify/refund", callbackUrl));

        request.setSign(sign(request).toUpperCase());
        MiniProgramPayRefundResponse response;
        try {
            response = httpsRequest(request, keyStore, certPassword);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        if (!"SUCCESS".equalsIgnoreCase(response.getResultCode())) {
            throw new WeChatException(response.getErrCode(), response.getErrCodeDes());
        }

        log.info("退款请求：{} 返回结果：{}", JacksonUtils.toJSONString(request), JacksonUtils.toJSONString(response));

        Tuple<MiniProgramPayRefundRequest, MiniProgramPayRefundResponse> result = new Tuple<>();
        result.setA(request);
        result.setB(response);

        return result;
    }

    /**
     * 交易订单查询
     */
    public MiniProgramPayOrderQueryResponse queryOrder(String outTradeId, String transactionId) {
        transactionId = StringUtils.trimToNull(transactionId);

        MiniProgramPayOrderQueryRequest request = new MiniProgramPayOrderQueryRequest();
        request.setAppid(getAppId());
        request.setMchId(mchId);
        request.setOutTradeNo(outTradeId);
        request.setTransactionId(transactionId);
        request.setNonceStr(TokenGenerate.getToken());
        MiniProgramPayOrderQueryResponse response = request(request);

        if (!"SUCCESS".equalsIgnoreCase(response.getResultCode())) {
            throw new WeChatException(response.getErrCode(), response.getErrCodeDes());
        }
        return response;
    }

    /**
     * 关闭订单
     */
    public void closeOrder(String outTradeNo) {
        if (StringUtils.isBlank(outTradeNo)) {
            throw new IllegalArgumentException("outTradeNo为空！");
        }

        MiniProgramPayCloseOrderRequest request = new MiniProgramPayCloseOrderRequest();
        request.setAppid(getAppId());
        request.setMchId(mchId);
        request.setOutTradeNo(outTradeNo);
        request.setNonceStr(TokenGenerate.getToken());
        MiniProgramPayCloseOrderResponse response = request(request);

        if (!"SUCCESS".equalsIgnoreCase(response.getResultCode())) {
            throw new WeChatException(response.getErrCode(), response.getErrCodeDes());
        }
    }


    @Override
    public String sign(Object obj) {
        RequestMetaInfo metaInfo = MetaInfoCache.parseRequest(obj.getClass());

        ArrayList<RequestMetaInfo.RequestParamInfo> fields = new ArrayList<>(metaInfo.getParamInfo().values());
        fields.sort(Comparator.comparing(f -> f.getField().getName()));

        List<String> signItems = fields.stream()
                .filter(f -> !"sign".equalsIgnoreCase(f.getField().getName()))
                .map(f -> {
                    try {
                        Object invokeGetValue = f.getGetter().invoke(obj);
                        String value = invokeGetValue == null ? "" : StringUtils.trimToEmpty(String.valueOf(invokeGetValue));
                        String fieldName = f.getWeChatApiField().value();
                        if (StringUtils.isBlank(fieldName)) {
                            fieldName = f.getWeChatApiField().name();
                        }
                        if (StringUtils.isBlank(fieldName)) {
                            fieldName = f.getField().getName();
                        }
                        return fieldName + "=" + value;
                    } catch (ReflectiveOperationException e) {
                        throw new IllegalStateException("???", e);
                    }
                })
                .filter(i -> !i.endsWith("="))
                .collect(Collectors.toList());
        signItems.add("key=" + mchPayKey);

        return MD5Utils.md5AndHex(StringUtils.join(signItems, "&"), null);
    }

    @Override
    public String signByXml(String xml) {
        SAXBuilder builder = new SAXBuilder();
        try {
            Document doc = builder.build(new StringReader(xml));
            Element root = doc.getRootElement();
            List children = root.getChildren();
            List<Tuple<String, String>> tuples = new ArrayList<>();
            for (Object child : children) {
                Element ele = (Element) child;
                String tagName = ele.getName();
                String value = ele.getValue();
                if (!"sign".equalsIgnoreCase(tagName)) {
                    Tuple<String, String> tuple = new Tuple<>();
                    tuple.setA(tagName);
                    tuple.setB(value);
                    tuples.add(tuple);
                }
            }
            List<String> signItems = tuples.stream()
                    .filter(f -> !"sign".equalsIgnoreCase(f.getA()))
                    .map(f -> f.getA() + "=" + f.getB())
                    .filter(i -> !i.endsWith("="))
                    .collect(Collectors.toList());
            signItems.add("key=" + mchPayKey);

            return MD5Utils.md5AndHex(StringUtils.join(signItems, "&"), null);
        }  catch (Exception e) {
            throw new IllegalStateException("???", e);
        }
    }

    @Override
    public String decode(String code) {
        return aes.decryptStr(code);
    }
}
